/*
 * $Id: QEADIFParser.m 86 2009-07-14 02:52:39Z jon@bway.net $
 * Chalk
 *
 * Created 10/6/08
 * Copyright Jon Gordon
 * 
 * Permission is hereby granted, free of charge, to any person obtaining 
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including 
 * without limitation the rights to use, copy, modify, merge, publish, 
 * distribute, sublicense, and/or sell copies of the Software, and to 
 * permit persons to whom the Software is furnished to do so, subject to 
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
#include <wchar.h>

#import "QEADIFParser.h"

#import "QEADIFInternal.h"

#import "QEADIFElement.h"

#define DEFAULT_SOURCE @""

#define EOI ((unichar) 0xffff)
#define OPEN_CHAR ((unichar) '<')
#define SEPARATOR_CHAR ((unichar) ':')
#define CLOSE_CHAR ((unichar) '>')

#define FIELD_NAME_CHARACTERS @"_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
#define DIGIT_CHARACTERS      @"0123456789"
#define TYPE_CODE_CHARACTERS  @"abdlmnstABDLMNST"

static NSCharacterSet *FIELD_NAME_CHARACTER_SET;
static NSCharacterSet *DIGIT_CHARACTER_SET;
static NSCharacterSet *TYPE_CODE_CHARACTER_SET;

@interface QEADIFParser (private)
#pragma mark INTERNAL PROCCESSING
- (void)nextChar;
- (BOOL)isEOI;

- (void)makeNextElement;

- (void)handleEOI;
- (void)handleComment;
- (void)handleFieldNameExpected;
- (void)handleFieldName;
- (void)handleLengthExpected;
- (void)handleLength;
- (void)handleTypeCodeExpected;
- (void)handleCloseExpected;
- (void)handleTagClose;
- (void)handleData;
- (void)handleErrorUnexpectedEOI;
- (void)handleErrorZeroLengthFieldName;
- (void)handleErrorBadFieldName;
- (void)handleErrorLengthExpected;
- (void)handleErrorBadTypeCode;
- (void)handleErrorMissingTypeCode;
- (void)handleErrorCloseMissing;

#pragma mark OUTPUT GENERATORS
- (void)enqueueItem:(QEADIFElement *)element;
@end


@implementation QEADIFParser
#pragma mark LIFECYCLE METHODS
+ (BOOL)accessInstanceVariablesDirectly
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    return NO;
} // accessInstanceVariablesDirectly

+ (void)initialize
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    static BOOL initialized = NO;
    
    if (!initialized) {
        //-NSLog(@"  initializing");
        initialized = YES;
        FIELD_NAME_CHARACTER_SET = [[NSCharacterSet characterSetWithCharactersInString:FIELD_NAME_CHARACTERS] retain];
        DIGIT_CHARACTER_SET =      [[NSCharacterSet characterSetWithCharactersInString:DIGIT_CHARACTERS] retain];
        TYPE_CODE_CHARACTER_SET =  [[NSCharacterSet characterSetWithCharactersInString:TYPE_CODE_CHARACTERS] retain];
    } // if
} // initialize

- (id)init
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    return [self initWithSrc:nil];
} // init

- (id)initWithSrc:(const NSString *)source
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    if (nil == source) {
        source = DEFAULT_SOURCE;
    } // if
    
    self = [super init];
    if (nil != self) {
        outQueue = [[NSMutableArray alloc] initWithCapacity:2];
        
        src = [source copy];
        reachedEOI = NO;
        charIdx = 0;
        if ([self isEOI]) {
            c = EOI;
        } else {
            c = [src characterAtIndex:charIdx];
        } // else
        line = 1;
        pos = 1;
        if ( ('\n' == c) ||
            ( ('\r' == c) && 
             ([src length]  > 1) && 
             ('\n' == [src characterAtIndex:charIdx]) ) )  {
            isNewLine = YES;
        } else {
            isNewLine = NO;
        } // else
    } // if
    return self;
} // init

- (void)dealloc
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    [src release];
    [outQueue release];
    [super dealloc];
} // dealloc


#pragma mark DATA OPERATIONS
+ (NSMutableArray *)allElementsFromSource:(NSString *)src
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    if (nil == src) {
        return nil;
    } // if
    
    NSMutableArray *result = [[NSMutableArray alloc] init];
    QEADIFParser *parser = [[QEADIFParser alloc] initWithSrc:src];
    while ([parser hasMoreElements]) {
        [result addObject:[parser nextElement]];
    } // while
    [parser release];
    return [result autorelease];
} // allElementsFromSource:

+ (NSMutableArray *)cleanedElementsFromSource:(NSString *)src
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    if (nil == src) {
        return nil;
    } // if
    
    NSMutableArray *result = [[NSMutableArray alloc] init];
    QEADIFParser *parser = [[QEADIFParser alloc] initWithSrc:src];
    while ([parser hasMoreElements]) {
        QEADIFElement *element = [parser nextElement];
        if ([element isComment]) {
            NSString *trimSrc = [[element src] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
            if ((nil == trimSrc) || 
                ((0 < [result count]) && (0 == [trimSrc length])) ) {
                continue;   // omit empty comments
            } else {
                [element setSrc:trimSrc];
            } // else
        } else if ([element isTag]) {
            [element setFieldName:[[element fieldName] uppercaseString]];
        } else if ([element isField]) {
            [element setFieldName:[[element fieldName] uppercaseString]];
            if (qeADIFDataTypeUnspecified != [element typeCode]) {
                [element setTypeCode:towlower([element typeCode])];
            } // if
        } // else if
        [result addObject:element];
    } // while
    [parser release];
    return [result autorelease];
} // cleanedElementsFromSource:


- (QEADIFElement *)nextElement
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    id result = nil;
    
    if (0 < [outQueue count]) {
        result = [outQueue objectAtIndex:0];
        [outQueue removeObjectAtIndex:0];
    } else if (!reachedEOI) {
        [self makeNextElement];
        if (0 < [outQueue count]) {
            result = [outQueue objectAtIndex:0];
            [outQueue removeObjectAtIndex:0];
        } // if
    } // if
    
    //-NSLog(@"  returning: %@", result);
    return result;
} // nextElement

- (BOOL)hasMoreElements
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    return !reachedEOI || (0 < [outQueue count]);
} // hasMoreElements


#pragma mark INTERNAL PROCESSING
- (void)nextChar
{
    if (reachedEOI) {
        //        //-NSLog(@"  EOI flagged");
        c = EOI;
    } else {
        NSAssert([src length] > charIdx,
                 @"EOI should have been flagged already");
        ++charIdx;
        
        if ([self isEOI]) {
            c = EOI;
        } else {
            c = [src characterAtIndex:charIdx];
        } // if
        
        /*
         * After retrieving the next character, we either increment the position
         * along the line or start a new line by returning to position 1 and
         * incrementing the line number.
         *
         * A line ends with one of three characters or sequences: 1) CR, 2) LF, 
         * and 3) CR followed by LF.  We therefore apply the following rules:
         *
         *    1) LF always means the end of a line.
         *
         *    2) CR means the end of a line unless immediately followed by LF.
         */
        if (isNewLine) {
            ++line;
            pos = 1;
            isNewLine = NO;
        } else {
            ++pos;
        } // else
        
        if ( ('\n' == c) ||
            ( ('\r' == c) && 
             (([src length] - 1) > charIdx) && 
             ('\n' == [src characterAtIndex:charIdx]) ) )  {
            isNewLine = YES;
        } // if
    } // else
    
    // BEGIN DEBUG
    if (EOI == c) {
        //        //-NSLog(@"  c:EOI, %lu, %lu", line, pos);
    } else {
        //        //-NSLog(@"  c:\'%C\', %lu, %lu", c, line, pos);
    } // else
    // END DEBUG
} // nextChar

- (BOOL)isEOI
{
    return ([src length] == charIdx);
} // isEOI


- (void)makeNextElement
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSAssert(!reachedEOI,
             @"asked for more elements after EOI already seen");
    
    elementStartIdx = charIdx;
    startLine = line;
    startPos = pos;
    
    typeCode = qeADIFDataTypeUnspecified;
    
    switch (c) {
        case EOI:
            [self handleEOI];
            break;
        case OPEN_CHAR:
            [self nextChar];
            [self handleFieldNameExpected];
            break;
        default:
            [self handleComment];
            break;
    } // switch(c)
} // makeNextElement


- (void)handleEOI
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    reachedEOI = YES;
    [self nextChar];
    [self enqueueItem:[QEADIFElement elementEOIAtLine:startLine
                                          andPosition:startPos]];
} // handleEOI

- (void)handleComment
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSUInteger startIdx = charIdx;
    NSUInteger count = 0;
    while ((c != EOI) && (c != OPEN_CHAR)) {
        ++count;
        [self nextChar];
    } // while
    
    NSRange commentRange = { startIdx, count };
    [self enqueueItem:[QEADIFElement elementCommentFromSource:[src substringWithRange:commentRange] 
                                                       atLine:startLine 
                                                  andPosition:startPos]];
} // handleComment

- (void)handleFieldNameExpected
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    if (EOI == c) {
        [self handleErrorUnexpectedEOI];
    } else if ((CLOSE_CHAR == c) || (SEPARATOR_CHAR == c)) {
        [self handleErrorZeroLengthFieldName];
    } else if ([FIELD_NAME_CHARACTER_SET characterIsMember:c]) {
        [self handleFieldName];
    } else {
        [self handleErrorBadFieldName];
    } // else
} // handleFieldNameExpected

- (void)handleFieldName
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSUInteger startIdx = charIdx;
    NSUInteger count = 0;
    while ((c != EOI) && [FIELD_NAME_CHARACTER_SET characterIsMember:c]) {
        ++count;
        [self nextChar];
    } // while
    
    if (EOI == c) {
        [self handleErrorUnexpectedEOI];
    } else if ((SEPARATOR_CHAR == c) || (CLOSE_CHAR == c)) {
        NSRange fieldNameRange = { startIdx, count };
        NSAssert((startIdx + count) <= [src length],
                 @"Field name extends beyond end of source.");
        fieldName = [src substringWithRange:fieldNameRange];
        //        //-NSLog(@"  field name is \"%@\"", fieldName);
        if (SEPARATOR_CHAR == c) {
            [self nextChar];
            [self handleLengthExpected];
        } else {
            [self nextChar];
            [self handleTagClose];
        } // else
    } else {
        NSRange fieldNameRange = { startIdx, count + 1 };
        fieldName = [src substringWithRange:fieldNameRange];
        [self handleErrorBadFieldName];
    } // else
} // handleFieldName

- (void)handleLengthExpected
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    if (EOI == c) {
        [self handleErrorUnexpectedEOI];
    } else if ([DIGIT_CHARACTER_SET characterIsMember:c]) {
        [self handleLength];
    } else {
        [self handleErrorLengthExpected];
    }
} // handleLengthExpected

- (void)handleLength
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSUInteger startIdx = charIdx;
    NSUInteger count = 0;
    while ((c != EOI) && [DIGIT_CHARACTER_SET characterIsMember:c]) {
        ++count;
        [self nextChar];
    } // while
    
    if (EOI == c)  {
        [self handleErrorUnexpectedEOI];
    } else if ((CLOSE_CHAR == c) || (SEPARATOR_CHAR == c)) {
        NSRange dataLengthRange = { startIdx, count };
        NSAssert((startIdx + count) <= [src length],
                 @"String representing data length extends beyond source.");
        dataLength = [[src substringWithRange:dataLengthRange] integerValue];
        //            //-NSLog(@"  data should be %ld characters long", \
        ((long) dataLength));
        if (CLOSE_CHAR == c) {
            [self nextChar];
            [self handleData];
        } else {
            [self nextChar];
            [self handleTypeCodeExpected];
        } // else
    } else {
        [self handleErrorLengthExpected];
    } // else
} // handleLength

- (void)handleTypeCodeExpected
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    if ([TYPE_CODE_CHARACTER_SET characterIsMember:c]) {
        typeCode = c;
        [self nextChar];
        [self handleCloseExpected];
    } else if (CLOSE_CHAR == c) {
        [self handleErrorMissingTypeCode];
    } else {
        [self handleErrorBadTypeCode];
    } // else
} // handleTypeCodeExpected

- (void)handleCloseExpected
{
    if (CLOSE_CHAR == c) {
        [self nextChar];
        [self handleData];
    } else {
        [self handleErrorCloseMissing];
    } // else
} // handleCloseExpected

- (void)handleTagClose
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSRange srcRange = { elementStartIdx , charIdx - elementStartIdx };
    [self enqueueItem:[QEADIFElement elementTagWithFieldName:fieldName
                                                  fromSource:[src substringWithRange:srcRange]
                                                      atLine:startLine
                                                 andPosition:startPos]];
} // handleTagClose

- (void)handleData
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSUInteger startIdx = charIdx;
    NSInteger  count = 0;
    while ((c != EOI) && (count < dataLength)) {
        ++count;
        [self nextChar];
    } // while
    if (count < dataLength) {
        [self handleErrorUnexpectedEOI];
    } else {
        NSRange srcRange = { elementStartIdx , charIdx - elementStartIdx };
        NSRange dataRange = { startIdx, dataLength };
        fieldValue = [src substringWithRange:dataRange];
        //        //-NSLog(@"  field value: \"%@\"", fieldValue);
        [self enqueueItem:[QEADIFElement elementFieldWithFieldName:fieldName
                                                        fieldValue:fieldValue
                                                            ofType:typeCode
                                                        fromSource:[src substringWithRange:srcRange]
                                                            atLine:startLine
                                                       andPosition:startPos]];
    } // else
} // handleData

#pragma mark --parse errors
- (void)handleErrorUnexpectedEOI
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSString *formatStr = NSLocalizedString(@"Syntax error at line %lu, position %lu: unexpected end of input.",
                                            @"");
    NSString *eMsg = [NSString stringWithFormat:formatStr, line, pos];
    NSDictionary *eDict = [NSDictionary dictionaryWithObjectsAndKeys:eMsg, NSLocalizedDescriptionKey, nil];
    NSError *error = [NSError errorWithDomain:QEADIFErrorDomain
                                         code:QEADIFErrorSynEOI
                                     userInfo:eDict];
    NSRange srcRange = { elementStartIdx , charIdx - elementStartIdx };
    [self enqueueItem:[QEADIFElement elementError:error
                                       fromSource:[src substringWithRange:srcRange]
                                           atLine:line 
                                      andPosition:pos]];
    
    startLine = line;
    startPos = pos;
    [self handleEOI];
} // handleErrorUnexpectedEOI

- (void)handleErrorZeroLengthFieldName
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSString *formatStr = NSLocalizedString(@"Syntax error at line %lu, position %lu: missing name of field or tag.",
                                            @"");
    NSString *eMsg = [NSString stringWithFormat:formatStr, line, pos];
    NSDictionary *eDict = [NSDictionary dictionaryWithObjectsAndKeys:eMsg, NSLocalizedDescriptionKey, nil];
    NSError *error = [NSError errorWithDomain:QEADIFErrorDomain
                                         code:QEADIFErrorSynNoName
                                     userInfo:eDict];
    NSRange srcRange = { elementStartIdx , charIdx - elementStartIdx };
    [self enqueueItem:[QEADIFElement elementError:error
                                       fromSource:[src substringWithRange:srcRange]
                                           atLine:line 
                                      andPosition:pos]];
    
    [self nextChar];
} // handleErrorZeroLengthFieldName

- (void)handleErrorBadFieldName
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSString *formatStr = NSLocalizedString(@"Syntax error at line %lu, position %lu: illegal character in name of field or tag \"%@\".",
                                            @"");
    NSString *eMsg = [NSString stringWithFormat:formatStr, line, pos, fieldName];
    NSDictionary *eDict = [NSDictionary dictionaryWithObjectsAndKeys:eMsg, NSLocalizedDescriptionKey, nil];
    NSError *error = [NSError errorWithDomain:QEADIFErrorDomain
                                         code:QEADIFErrorSynBadName
                                     userInfo:eDict];
    NSRange srcRange = { elementStartIdx , charIdx - elementStartIdx };
    [self enqueueItem:[QEADIFElement elementError:error
                                       fromSource:[src substringWithRange:srcRange]
                                           atLine:line 
                                      andPosition:pos]];
    
    [self nextChar];
}  // handleErrorBadFieldName

- (void)handleErrorLengthExpected
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSRange srcRange = { elementStartIdx , charIdx - elementStartIdx + 1 };
    NSString *formatStr = NSLocalizedString(@"Syntax error at line %lu, position %lu: data length expected but missing from \"%@\".",
                                            @"");
    NSString *eMsg = [NSString stringWithFormat:formatStr, line, pos, [src substringWithRange:srcRange]];
    NSDictionary *eDict = [NSDictionary dictionaryWithObjectsAndKeys:eMsg, NSLocalizedDescriptionKey, nil];
    NSError *error = [NSError errorWithDomain:QEADIFErrorDomain
                                         code:QEADIFErrorSynBadLength
                                     userInfo:eDict];
    [self enqueueItem:[QEADIFElement elementError:error
                                       fromSource:[src substringWithRange:srcRange]
                                           atLine:line 
                                      andPosition:pos]];
    
    [self nextChar];
} // handleErrorLengthExpected

- (void)handleErrorMissingTypeCode
{
    //-NSLog(@"MESSAGE: %s:", __PRETTY_FUNCTION__);
    NSRange srcRange = { elementStartIdx , charIdx - elementStartIdx + 1};
    NSString *formatStr = NSLocalizedString(@"Syntax error at line %lu, position %lu: missing type code from field \"%@\".",
                                            @"");
    NSString *eMsg = [NSString stringWithFormat:formatStr, line, pos, [src substringWithRange:srcRange]];
    NSDictionary *eDict = [NSDictionary dictionaryWithObjectsAndKeys:eMsg, NSLocalizedDescriptionKey, nil];
    NSError *error = [NSError errorWithDomain:QEADIFErrorDomain
                                         code:QEADIFErrorSynMissingType
                                     userInfo:eDict];
    [self enqueueItem:[QEADIFElement elementError:error
                                       fromSource:[src substringWithRange:srcRange]
                                           atLine:line 
                                      andPosition:pos]];
} // handleMissingTypeCode

- (void)handleErrorBadTypeCode
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSRange srcRange = { elementStartIdx , charIdx - elementStartIdx + 1};
    NSString *formatStr = NSLocalizedString(@"Syntax error at line %lu, position %lu: invalid type code encountered in field \"%@\".",
                                            @"");
    NSString *eMsg = [NSString stringWithFormat:formatStr, line, pos, [src substringWithRange:srcRange]];
    NSDictionary *eDict = [NSDictionary dictionaryWithObjectsAndKeys:eMsg, NSLocalizedDescriptionKey, nil];
    NSError *error = [NSError errorWithDomain:QEADIFErrorDomain
                                         code:QEADIFErrorSynBadType
                                     userInfo:eDict];
    [self enqueueItem:[QEADIFElement elementError:error
                                       fromSource:[src substringWithRange:srcRange]
                                           atLine:line 
                                      andPosition:pos]];
    
    [self nextChar];
} // handleErrorBadTypeCode

- (void)handleErrorCloseMissing
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSRange srcRange = { elementStartIdx , charIdx - elementStartIdx };
    NSString *formatStr = NSLocalizedString(@"Syntax error at line %lu, position %lu: closing angle bracket (\'>\') missing from field \"%@\".",
                                            @"");
    NSString *eMsg = [NSString stringWithFormat:formatStr, line, pos, [src substringWithRange:srcRange]];
    NSDictionary *eDict = [NSDictionary dictionaryWithObjectsAndKeys:eMsg, NSLocalizedDescriptionKey, nil];
    NSError *error = [NSError errorWithDomain:QEADIFErrorDomain
                                         code:QEADIFErrorSynNoClose
                                     userInfo:eDict];
    [self enqueueItem:[QEADIFElement elementError:error
                                       fromSource:[src substringWithRange:srcRange]
                                           atLine:line 
                                      andPosition:pos]];
} // handleErrorCloseMissing


#pragma mark OUTPUT GENERATORS
- (void)enqueueItem:(QEADIFElement *)element
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    [outQueue insertObject:element atIndex:[outQueue count]];
} // enqueueItem
@end
